package vote

import (
	caPkg "HeadLineNews/pkg/db/cache"
	"HeadLineNews/pkg/db/store"
	"HeadLineNews/pkg/zaplog"
	models "HeadLineNews/proto_models/app/vote_model"
	"HeadLineNews/resp"
	"HeadLineNews/utils/gin_ctx"
	"HeadLineNews/utils/transaction"
	context2 "context"
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis/v8"
	"github.com/jmoiron/sqlx"
	"go.uber.org/zap"
	"time"
)

const (
	newsCreateTimeZsetKey   = "news_create_time_zset" // 存放新闻创建时间的 zset
	newsScoreZsetKey        = "news_score_zset"       // 存放新闻得分的 zset
	newsVotedUserZsetKeyFmt = "news:%s:voted_user"    // 某一条新闻的用户点赞记录
	voteExpireTime          = 18000                   // 自新闻创建时间起 1800 秒内为有效点赞时间
	like                    = 1
	noLike                  = -1
)

var (
	context                        = context2.Background()
	errNewsNotExistsOrMissVoteTime = errors.New("新闻不存在或投票时间已过")
	errMissedNewsVoteTime          = errors.New("投票时间已过")
)

type Cache interface {
	CacheNewsCreateTime(newsId string) (err error)
	GetNewsScoreByNewsId(newsId string) (score float64, err error)
	GetNewsUserVoteRecord(newsId, userId string) (voteRecord float64, err error)
	GetNewsScoreByNewsIds(newsIds ...string) (scores []float64, err error)
	GetNewsUserVoteRecords(userId string, newsIds ...string) (voteRecords []float64, err error)

	UserVote(c *gin.Context, p *models.RequestUserVote) (err error)
	SyncScoreToMysql()
}

type cache struct {
	cli *redis.Client
}

func NewCache() Cache {
	return &cache{cli: caPkg.UserVoteCache()}
}

func GetVoteExpireTime() int64 {
	return voteExpireTime
}

// CacheNewsCreateTime 将新闻的创建时间进行缓存
func (cache *cache) CacheNewsCreateTime(newsId string) (err error) {
	tx := cache.cli.TxPipeline()
	defer func() { _ = tx.Close() }()
	tx.ZAdd(context, newsCreateTimeZsetKey, &redis.Z{
		Score:  float64(time.Now().Unix()),
		Member: newsId,
	})
	tx.ZIncrBy(context, newsScoreZsetKey, 0, newsId) // 初始 0 分
	_, err = tx.Exec(context)
	if err != nil {
		_ = tx.Discard()
	}
	return
}

// GetNewsScoreByNewsId 查询新闻得分
func (cache *cache) GetNewsScoreByNewsId(newsId string) (score float64, err error) {
	return cache.cli.ZScore(context, newsScoreZsetKey, newsId).Result()
}

// GetNewsUserVoteRecord 查看用户对某一条新闻的点赞记录
func (cache *cache) GetNewsUserVoteRecord(newsId, userId string) (voteRecord float64, err error) {
	key := fmt.Sprintf(newsVotedUserZsetKeyFmt, newsId)
	return cache.cli.ZScore(context, key, userId).Result()
}

// GetNewsScoreByNewsIds 批量查询新闻得分
func (cache *cache) GetNewsScoreByNewsIds(newsIds ...string) (scores []float64, err error) {
	tx := cache.cli.TxPipeline()
	defer func() { _ = tx.Close() }()
	for _, newsId := range newsIds {
		tx.ZScore(context, newsScoreZsetKey, newsId)
	}
	var cmdrs []redis.Cmder
	if cmdrs, err = tx.Exec(context); err != nil {
		_ = tx.Discard()
		zaplog.CallerL().Error("query news score failed", zap.String("error", err.Error()))
		return
	}
	var score float64
	for _, cmdr := range cmdrs {
		if score, err = cmdr.(*redis.FloatCmd).Result(); err != nil {
			zaplog.CallerL().Error("get news score failed", zap.String("error", err.Error()))
			return
		}
		scores = append(scores, score)
	}
	return
}

// GetNewsUserVoteRecords 查看用户对某一批条新闻的点赞记录
func (cache *cache) GetNewsUserVoteRecords(userId string, newsIds ...string) (voteRecords []float64, err error) {
	tx := cache.cli.TxPipeline()
	defer func() { _ = tx.Close() }()
	for _, newsId := range newsIds {
		key := fmt.Sprintf(newsVotedUserZsetKeyFmt, newsId)
		tx.ZScore(context, key, userId)
	}
	var cmdrs []redis.Cmder
	if cmdrs, err = tx.Exec(context); err != nil {
		if err == redis.Nil {
			err = nil
		}
		if err != redis.Nil {
			_ = tx.Discard()
			zaplog.CallerL().Error("query news vote record failed", zap.String("error", err.Error()))
			return
		}

	}
	var voteRecord float64
	for _, cmdr := range cmdrs {
		if voteRecord, err = cmdr.(*redis.FloatCmd).Result(); err != nil {
			// 为 nil 的时候直接添加为 0
			if err == redis.Nil {
				err = nil
			}
			if err != redis.Nil {
				zaplog.CallerL().Error("get news vote record failed", zap.String("error", err.Error()))
				return
			}
		}
		voteRecords = append(voteRecords, voteRecord)
	}

	return
}

// UserVote 用户点赞
func (cache *cache) UserVote(c *gin.Context, p *models.RequestUserVote) (err error) {
	// 检查是否过了点赞时间
	var newsCreateTime float64
	if newsCreateTime, err = cache.cli.ZScore(context, newsCreateTimeZsetKey, p.NewsId).Result(); err != nil {
		if err == redis.Nil {
			err = errNewsNotExistsOrMissVoteTime
			zaplog.L().Warn("invalid newsId or missed vote time", zaplog.Fields(c, p.NewsId, nil)...)
			resp.UnprovedParam(c, err.Error())
			return
		}
		zaplog.L().Error("query news create time failed", zaplog.Fields(c, p.NewsId, err)...)
		resp.ServerBusy(c)
		return
	}
	if time.Now().Unix()-int64(newsCreateTime) > voteExpireTime {
		err = errMissedNewsVoteTime
		zaplog.L().Warn("missed vote time", zaplog.Fields(c, p.NewsId, nil)...)
		resp.UnprovedParam(c, err.Error())
		return
	}

	// 查看某条新闻用户的历史点赞记录
	key := fmt.Sprintf(newsVotedUserZsetKeyFmt, p.NewsId)
	var oldLike float64
	if oldLike, err = cache.cli.ZScore(context, key, c.GetString(gin_ctx.UserId)).Result(); err != nil {
		if err == redis.Nil {
			err = nil
			oldLike = 0
		} else {
			zaplog.L().Error("query vote record failed",
				zaplog.Fields(c, fmt.Sprintf("key: %s, userId: %s", key, c.GetString(gin_ctx.UserId)), err)...)
			resp.ServerBusy(c)
			return
		}
	}
	// 点赞
	var score, newLike float64
	if float64(p.Like) == oldLike { // 两次点赞一样的话
		newLike = 0 // 新的投票记录全为 0
		switch oldLike {
		case like: // 之前点 1，这次还是 1，就 -1
			score = -1
		case noLike: // 之前 -1，这次还是 -1，就 +1
			score = 1
		}
	} else { // 不相等直接取差值
		newLike = float64(p.Like)
		score = float64(p.Like) - oldLike
	}

	tx := cache.cli.TxPipeline()
	defer func() { _ = tx.Close() }()
	// 更新当前用户对该新闻的点赞信息
	tx.ZAdd(context, key, &redis.Z{
		Score:  newLike,
		Member: c.GetString(gin_ctx.UserId),
	})
	// 更新该新闻的分数
	tx.ZIncrBy(context, newsScoreZsetKey, score, p.NewsId)
	if _, err = tx.Exec(context); err != nil {
		_ = tx.Discard()
		zaplog.L().Error("update news user vote record and score failed",
			zaplog.Fields(c, fmt.Sprintf("newsId: %s, score: %f", p.NewsId, score), err)...)
		resp.ServerBusy(c)
		return
	}
	return
}

// SyncScoreToMysql 将新闻的点赞数量同步到 mysql
func (cache *cache) SyncScoreToMysql() {
	// 查询所有新闻的创建时间
	newsCreateTimeSl, err := cache.cli.ZRangeWithScores(context, newsCreateTimeZsetKey, 0, -1).Result()
	if err != nil {
		zaplog.S().Errorf("query news create time failed. error: %v", err)
		return
	}
	// 检查哪些新闻已经过了点赞时间，搜集起来
	var expiredNewsIds []string
	ts := float64(time.Now().Unix())
	for _, newsCreateTime := range newsCreateTimeSl {
		if ts-newsCreateTime.Score > voteExpireTime { // 只要超过点赞时间的
			expiredNewsIds = append(expiredNewsIds, newsCreateTime.Member.(string))
		}
	}
	// 查询这些新闻的得分（点赞数量）
	tx := cache.cli.TxPipeline()
	defer func() { _ = tx.Close() }()
	for _, v := range expiredNewsIds {
		tx.ZScore(context, newsScoreZsetKey, v)
	}
	// 查询这些新闻的得分
	var cmdrSl []redis.Cmder
	cmdrSl, err = tx.Exec(context) // 一次执行所有查询
	if err != nil {
		_ = tx.Discard()
		zaplog.S().Errorf("query news score failed. error: %v", err)
		return
	}
	var newsScoreSl []float64
	for _, cmdr := range cmdrSl {
		score, te := cmdr.(*redis.FloatCmd).Result()
		if te != nil {
			zaplog.S().Errorf("get news score failed. error: %v", te)
			return
		}
		newsScoreSl = append(newsScoreSl, score)
	}
	// 将分数同步到 mysql
	if err = transaction.Transaction(store.DB(), func(tx *sqlx.Tx) (te error) {
		for i := 0; i < len(expiredNewsIds); i++ {
			sqlStr := `UPDATE app__news SET news_score=? WHERE news_id=?`
			if _, te = tx.Exec(sqlStr, newsScoreSl[i], expiredNewsIds[i]); te != nil {
				zaplog.S().Errorf("update socre failed. error: %v", te)
				return
			}
		}
		return
	}); err != nil {
		return
	}
	// 将 redis 里面过期新闻相关的数据全部删除
	for _, nId := range expiredNewsIds {
		tx.ZRem(context, newsCreateTimeZsetKey, nId)               // 创建时间相关的删除
		tx.ZRem(context, newsScoreZsetKey, nId)                    // 删除这个新闻的得分
		tx.Del(context, fmt.Sprintf(newsVotedUserZsetKeyFmt, nId)) // 删除这个新闻的
	}
	if _, te := tx.Exec(context); te != nil {
		_ = tx.Discard()
		zaplog.S().Errorf("delete news vote record failed. error: %v", te)
		return
	}
}

func SyncScore() {
	for {
		time.Sleep(time.Second * 5) // 每十五秒同步一次
		zaplog.L().Debug("开始同步")
		NewCache().SyncScoreToMysql()
		zaplog.L().Debug("同步完成")
	}
}
